跳到主要内容

SpringCloud Zuul 网关

Zuul 是什么?

参考资料 官方文档

它就是微服务网关,Spring Cloud Gateway 是 Spring Cloud Finchley 版推出来的新组件,用来代替服务网关:Zuul

一般微服务架构中都必然会设计一个网关在里面,像 android、ios、pc 前端、微信小程序、H5 等等,不用去关心后端有几百个服务,就知道有一个网关,所有请求都往网关走,网关会根据请求中的一些特征,将请求转发给后端的各个服务。

而且有一个网关之后,还有很多好处,比如可以做统一的降级、限流、认证授权、安全,等等。

配置环境

因为这个 Zuul 也是一个服务,所以也需要注册到 eureka 里面

<!-- https://mvnrepository.com/artifact/org.springframework.cloudspring-cloud-starter-netflix-eureka-client -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

<!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-netflix-zuul-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>

然后添上启动配置

@SpringBootApplication
@EnableEurekaClient
@EnableZuulProxy
public class StudyZuulApplication {

public static void main(String[] args) {
SpringApplication.run(StudyZuulApplication.class, args);
}

}

在配置文件里配置

eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/

# 随便指定个名字
spring:
application:
name: zuulServer

server:
port: 8085

现在就可以直接通过服务名访问服务了

# http://ip:zuul端口号/服务名/请求方法
# 注意:这里默认服务名都是小写(坑)

http://localhost:8085/customer/search

配置监控信息

先引入依赖

<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

配置文件上加上

# 添加监控信息的配置文件(上线时记得关掉)
management:
endpoints:
web:
exposure:
include: "*"

然后访问下面这链接就能看到当前部署的服务

http://localhost:8085/actuator/routes

{
"/search/**": "search",
"/zuulserver/**": "zuulserver",
"/customer/**": "customer"
}

忽略配置

有两种忽略方式,根据服务名称,根据一定的格式进行忽略(文本格式)

zuul:
# 根据服务名称忽略
ignored-services: zuulserver
# 根据格式忽略(这个还能在上面的监控信息上看到,但是已经无法访问了)
ignored-patterns: "/**/zuulserver/**"

自定义服务配置

自定义服务名称方式 01

zuul:
routes:
# 这里填服务名称,后面是自定义的名称
search: /ss/**
customer: /cs/**

# 这时默认名称和自定义名称是共存的
# {
# "/ss/**": "search",
# "/cs/**": "customer",
# "/search/**": "search",
# "/zuulserver/**": "zuulserver",
# "/customer/**": "customer"
# }

# 如果不想出现默认名称加上忽略就行了(注意不要使用格式忽略,否则全部都无法使用了)
zuul:
ignored-services: "*"

自定义服务名称方式 02 这种方式可以配置更多的信息

zuul:
routes:
mytest01: # 自定义名称
path: /cs/** # 映射的路径
serviceId: customer # 服务名称
mytest02:
path: /ss/**
serviceId: search
ignored-services: "*"

# 返回的值
# {
# "/cs/**": "customer",
# "/ss/**": "search"
# }

灰度发布

参考资料 Spring cloud架构中利用zuul网关实现灰度发布功能

灰度发布(又名金丝雀发布)是通过切换线上并存版本之间的路由权重,逐步从一个版本切换为另一个版本的过程。

在其上可以进行 A/B testing,即让一部分用户继续用产品特性 A,一部分用户开始用产品特性 B,如果用户对B没有什么反对意见,那么逐步扩大范围,把所有用户都迁移到 B 上面来。灰度发布可以保证整体系统的稳定,在初始灰度的时候就可以发现、调整问题,以保证其影响度。灰度期:灰度发布开始到结束期间的这一段时间,称为灰度期。(来源于百度百科)

在 Bean 上配置这个版本命名规则

@SpringBootApplication
@EnableEurekaClient
@EnableZuulProxy
public class StudyZuulApplication {

public static void main(String[] args) {
SpringApplication.run(StudyZuulApplication.class, args);
}

@Bean
public PatternServiceRouteMapper serviceRouteMapper() {
return new PatternServiceRouteMapper(
"(?<name>^.+)-(?<version>v.+$)",
"${version}/${name}");
// 服务名-v版本
// v版本/路径
}
}

在配置文件上加上版本号

version: v1
spring:
application:
name: Customer-${version}

# =========================================
# 复制一个模块设版本为 v2
version: v2
spring:
application:
name: Customer-${version}

注意注掉忽略服务的这个配置

zuul:
# ignored-services: "*"

再次访问 http://localhost:8085/actuator/routes 可以看到这两个服务已经用版本号区分开了

{
"/v2/customer/**": "customer-v2",
"/v1/customer/**": "customer-v1"
}

过滤器

过滤器执行流程

主要围绕着三种过滤器

PreFilter       前置过滤器(请求发过来就执行)
RoutingFilter 发送给服务之前和取得服务响应结果时执行
PostFilter 发送结果给客户端时执行
ErrorFilter 捕获错误

创建一个过滤器

@Component
public class TestZuulFilter extends ZuulFilter {
@Override
public String filterType() {
// 指定过滤器类型
return FilterConstants.PRE_TYPE;
}

@Override
public int filterOrder() {
// 数值越小优先级越高,官方推荐指定一个常量对其加减
return FilterConstants.PRE_DECORATION_FILTER_ORDER - 1;
}

@Override
public boolean shouldFilter() {
// 开启当前过滤器
return true;
}

@Override
public Object run() throws ZuulException {
// 这里执行具体的业务逻辑
System.out.println("这里是前置过滤器");
// 这里返回空不用管,因为返回啥都不会给处理
return null;
}
}

过滤器实现 Token 校验

@Component
public class CheckTokenFilter extends ZuulFilter {
@Override
public String filterType() {
// 指定过滤器类型
return FilterConstants.PRE_TYPE;
}

@Override
public int filterOrder() {
// 数值越小优先级越高,官方推荐指定一个常量对其加减
return FilterConstants.PRE_DECORATION_FILTER_ORDER - 1;
}

@Override
public boolean shouldFilter() {
// 开启当前过滤器
return true;
}

@Override
public Object run() throws ZuulException {
// 获取请求
RequestContext currentContext = RequestContext.getCurrentContext();
HttpServletRequest request = currentContext.getRequest();

// 取得 Token
String authorization = request.getHeader("Authorization");


// 判读 Token 是否正确 equalsIgnoreCase 不考虑大小写
if (!"1234".equalsIgnoreCase(authorization)) {
// 请求不会再发送给后面的过滤器了
currentContext.setSendZuulResponse(false);
// 响应给用户一个错误码
currentContext.setResponseStatusCode(HttpStatus.SC_UNAUTHORIZED);
System.out.println("校验失败");
return null;
}

// 这里执行具体的业务逻辑
System.out.println("校验成功");
return null;
}
}

Zuul 的降级

可以在 Zuul 统一处理没有做降级的方法(在 Zuul 中可以不用导入 Hystrix 依赖,因为它本身就自带了)

@Component
public class ZuulFallBack implements FallbackProvider {
@Override
public String getRoute() {
// 匹配所有方法,使之都走这个方法
return "*";
}

@Override
public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
System.out.println("被降级的服务名称" + route);
// 抛出的异常
cause.printStackTrace();
return new ClientHttpResponse() {
@Override
public HttpStatus getStatusCode() throws IOException {
// 抛出异常
return HttpStatus.INTERNAL_SERVER_ERROR;
}

@Override
public int getRawStatusCode() throws IOException {
// 抛出异常的状态码
return HttpStatus.INTERNAL_SERVER_ERROR.value();
}

@Override
public String getStatusText() throws IOException {
// 抛出异常原因
return HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase();
}

@Override
public void close() {
}

@Override
public InputStream getBody() throws IOException {
String msg = "当前服务:" + route + "出现了问题";
// 响应的数据
return new ByteArrayInputStream(msg.getBytes());
}

@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
return headers;
}
};
}
}

Zuul 的动态路由

就创建一个过滤器,把请求重定向到请求参数上指定的服务位置上

@Component
public class DynamicRoutingFilter extends ZuulFilter {
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}

@Override
public int filterOrder() {
// 最好放在所有 PRE 的后面
return FilterConstants.PRE_DECORATION_FILTER_ORDER + 1;
}

@Override
public boolean shouldFilter() {
// 开启当前过滤器
return true;
}

@Override
public Object run() throws ZuulException {
// 1、拿到 Request 对象
RequestContext context = RequestContext.getCurrentContext();
HttpServletRequest request = context.getRequest();

// 2、获取参数(redisKey)
String redisKey = request.getParameter("redisKey");

// 3、判断
if ("customer".equalsIgnoreCase(redisKey)) {
// 指定跳转到哪个位置上,例如这里就是跳转到
// http://localhost:8080/search/2 (customer-v1 的那个服务)
context.put(FilterConstants.SERVICE_ID_KEY, "customer-v1"); // 服务的名称
context.put(FilterConstants.REQUEST_URI_KEY, "/search/2"); // 该服务下的方法(uri)
} else if ("search".equalsIgnoreCase(redisKey)) {
// http://localhost:8081/search/1 (search 的那个服务)
context.put(FilterConstants.SERVICE_ID_KEY, "search");
context.put(FilterConstants.REQUEST_URI_KEY, "/search/1");
}

return null;
}
}